今天要開始規劃我們的資料層,上週有提到 data layer 裡暴露及提供給其他層次的是 Repository (可參考第二天的圖),而 Repository 的資料則是由 Local/Remote DataSource 提供; 在本地端的 DataSource 則是由 Room 負責,下面我們來實現這個架構。
關於 Room 的介紹及原理等網路上都已經有很多很多教學,因此我會省略一些內容,想要了解詳細內容可以到官網學習。
首先是 gradle :
dependencies {
def roomVersion = "2.1.0"
implementation "androidx.room:room-runtime:$roomVersion"
kapt "androidx.room:room-compiler:$roomVersion"
implementation "androidx.room:room-ktx:$roomVersion"
}
接著建立一個用來表示工作事項資料的 Entity --- Task
,這會是接下來在 Room 裡使用的 Table :
@Entity(tableName = "Tasks")
data class Task (
@ColumnInfo(name = "title")
var title: String = "",
@ColumnInfo(name = "description")
var description: String = "",
@ColumnInfo(name = "completed")
var isCompleted: Boolean = false,
@PrimaryKey
@ColumnInfo(name = "entryid")
var id: String = UUID.randomUUID().toString()
) {
val titleForList: String
get() = if (title.isNotEmpty()) title else description
val isActive
get() = !isCompleted
val isEmpty
get() = title.isEmpty() || description.isEmpty()
}
這邊我們把 id
當作是 PrimaryKey ,其內容是一個 random 的 UUID。
再來實作定義 SQL 語句的 TasksDao
:
@Dao
interface TasksDao {
@Query("SELECT * FROM tasks")
suspend fun getTasks(): List<Task>
@Query("SELECT * FROM tasks WHERE entryid = :taskId")
suspend fun getTaskById(taskId: String): Task?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTask(task: Task): Long
@Update
suspend fun updateTask(task: Task): Int
@Query("UPDATE Tasks SET completed = :completed WHERE entryid = :taskId")
suspend fun updateComplete(taskId: String, completed: Boolean)
@Query("DELETE FROM Tasks WHERE entryid = :taskId")
suspend fun deleteTaskById(taskId: String): Int
@Query("DELETE FROM Tasks")
suspend fun deleteTasks()
@Query("DELETE FROM Tasks WHERE completed = 1")
suspend fun deleteCompletedTasks(): Int
}
值得一提的是現在 Room 已經支持使用 Kotlin Coroutines ,所以我的 TasksDao
已經改成了 suspend function 。
接著建立資料庫 ToDoRoomDatabase
:
@Database(entities = [Task::class], version = 1, exportSchema = false)
abstract class ToDoRoomDatabase : RoomDatabase() {
abstract fun taskDao(): TasksDao
}
一樣 Room 需要繼承 RoomDatabase
,這邊我們讓 DB 放入 Table Task
,一般來說開發時不太需要輸出 DB 的 schema ,除非有特殊用途例如 Database 的 Migration Unit Test 。
我們再寫一個可以獲取 Database 的 class ,這個 class 可以提供 Database 的實體:
object ServiceInjector {
@Volatile
private var database: ToDoDatabase? = null
fun createDataBase(context: Context): ToDoDatabase {
val result = Room.databaseBuilder(
context.applicationContext,
ToDoDatabase::class.java, "Tasks.db"
).build()
database = result
return result
}
}
如此可以這樣獲取資料:
val dao = ServiceInjector.createDateBase(context).tasksDao()
val tasks: List<Task> = dao.getTasks()
......
現在已經完成 Room 的初始化及 Dao 的使用,再來我們要處理架構的部分。
DataSource 顧名思義就是指資料的來源,我們會在這個層次將各種不同資料依據來源的不同而拆分。
其實嚴格上來說,這個專案因為比較簡單的緣故,是可以不需要拆出這一個層次,但是我們為了要模仿一般專案的情境,這裡就把 Task 資料的來源假設成有 Remote(Network) 及 Local(DataBase) 兩種,當然如果有多個資料來源可以再繼續拆分,這就依實際情況來決定。
首先我們使用一個 interface 來定義使用 Task 資料的方法:
interface TasksDataSource {
suspend fun getTasks(): Result<List<Task>>
suspend fun getTask(taskId: String): Result<Task>
suspend fun saveTask(task: Task)
suspend fun completeTask(task: Task)
suspend fun completeTask(taskId: String)
suspend fun activateTask(task: Task)
suspend fun activateTask(taskId: String)
suspend fun cleanCompleteTasks()
suspend fun deleteAllTasks()
suspend fun deleteTask(taskId: String)
}
接下來各種不同的資料來源再各自實作 interface ,就可以把資料來源拆開來。這邊其實沒有硬性規定必須使用同一個 interface ,只要把握住 "依據不同資料來源劃分" 這個思路即可。
由於程式碼稍長,這裡提供一個範例給大家餐考。
這是一個很常見的設計方式,簡單來說 Repository 大約是對應 MVC 裡面的 Model 層,負責管理資料的進出, Domain Layer 只管對 Data Layer 的 Repository 請求或是存取資料,不需要在乎 Repository 的資料來源是誰,實際要使用哪個資料來源由 Repository 自己決定。
我們會在 Repository 這個層次使用 Remote Data 及 Local Data ,具體要用哪個再根據實際狀況決定。首先我們先透過 interface 來定義 Repository 的方法:
interface ITasksRepository {
suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>>
suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Result<Task>
suspend fun saveTask(task: Task)
suspend fun completeTask(task: Task)
suspend fun completeTask(taskId: String)
suspend fun activateTask(task: Task)
suspend fun activateTask(taskId: String)
suspend fun clearCompletedTasks()
suspend fun deleteAllTasks()
suspend fun deleteTask(taskId: String)
}
由於這邊程式碼有點長,如果有興趣可以從這裡點擊進入查看詳細的作法。
最後在 ServiceInjector
補上獲得 DataSource 及 Repository 的方法即可。
object ServiceInjector {
......
fun createTasksRepository(context: Context): TasksRepository {
return DefaultTasksRepository(
FakeTasksRemoteDataSource,
createTaskLocalDataSource(context)
)
}
fun createTaskLocalDataSource(context: Context): TasksDataSource {
val database = database ?: createDataBase(context)
return TasksLocalDataSource(database.taskDao())
}
}
這樣就大致完成 Data Layer 的程式了。